Tested for Ubuntu 18.04
Using Fail2Ban.
To function properly fail2ban must know the correct log format. I use a customized format for my access.log (to include virtualhost), but have configured a special log file that only logs 404 errors in Common log format.
Installation
apt-get install fail2ban
Configuration
General
/etc/fail2ban/jail.local
# "bantime" is the number of seconds that a host is banned.
bantime = 1h
# A host is banned if it has generated "maxretry" during the last "findtime" seconds.
findtime = 10m
# Block unsucessful SSH accesses.
[sshd]
enabled = false #disabled as only pubkey
# Block unsucessful auth tries
[apache]
enabled = true
port = http,https
filter = apache-auth
logpath = /var/log/apache2/error.log
maxretry = 10
bantime = 10min
# Detect Scanners for "known" vulnerabilites that get 404s
[apache-knownscans]
enabled = true
port = http,https
filter = http-knownscans
logpath = /var/log/apache2/status404.log
maxretry = 8
bantime = 1h
/etc/fail2ban/filter.d/http-knownscans.conf
failregex = ^<HOST> -.*"(GET|POST) [^ "]+(?i)phpmyadmin
^<HOST> -.*"(GET|POST) [^ "]+(?i)phpinfo
^<HOST> -.*"(GET|POST) [^ "]+(?i)admin
^<HOST> -.*"(GET|POST) [^ "]+(?i)webdav
^<HOST> -.*"(GET|POST) [^ "]+(?i)db_
^<HOST> -.*"(GET|POST) [^ "]+(?i)pma
^<HOST> -.*"(GET|POST) [^ "]+(?i)composer
^<HOST> -.*"(GET|POST) [^ "]+(?i)drupal
^<HOST> -.*"(GET|POST) [^ "]+(?i)joomla
^<HOST> -.*"(GET|POST) [^ "]+(?i)wp_admin
^<HOST> -.*"(GET|POST) [^ "]+(?i)ftp
^<HOST> -.*"(GET|POST) [^ "]+(?i)lang\.php
^<HOST> -.*"(GET|POST) [^ "]+(?i)desktop\.ini
^<HOST> -.*"(GET|POST) [^ "]+(?i)mysql
^<HOST> -.*"(GET|POST) [^ "]+(?i)sql
ignoreregex =
Restart
systemctl restart fail2ban
fail2ban-client reload
Manually unban an IP: fail2ban-client set YOURJAILNAMEHERE unbanip IPADDRESSHERE